package com.container.app.utilities;

import com.container.app.ContainerProvider;
import com.container.app.annotations.*;
import com.container.app.exceptions.MContainerUncheckedException;
import com.container.app.proxy.ContainerProxyHandler;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.reflect.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

/**
 * Created with IntelliJ IDEA.
 * User: Georgiana.Scarlat
 * Date: 8/22/12
 * Time: 4:18 PM
 */
public class ContainerUtils {

    public static final String EMPTY_STRING = "";
    public static final String NAME = "name";
    public static final String SCOPE_STRING = "scope";
    public static final String SINGLETON = "Singleton";
    public static final String CLONE = "clone";
    public static final String FILE_SEPARATOR = "file.separator";
    public static final String VALUE = "value";
    public static final String DEPENDENCE = "dependence";
    public static final String CLASS_MAPPINGS_OUT = "class_mappings.out";
    public static final String OBJECT_INSTANCES_OUT = "objectInstances.out";
    public static final String MAPPINGS_OUT = "mappings.out";
    public static final String CONFIGURATION = "configuration";
    public static final String PACKAGE = "package";

    @SuppressWarnings("unchecked")
    public static Annotation getComponent(Class cls) {
        Class componentClasses[] = {Component.class, DAOComponent.class, ServiceComponent.class, ControllerComponent.class, ModelComponent.class};
        for (Class component : componentClasses)
            if (cls.isAnnotationPresent(component))
                return cls.getAnnotation(component);
        return null;
    }


    public static String getComponentMethodValue(Class cls, String methodName) {

        Annotation annotation = getComponent(cls);

        if (annotation == null)
            throw new IncompleteAnnotationException(Component.class, methodName);

        try {

            annotation.annotationType().getMethod(methodName).setAccessible(true);
            return (String) annotation.annotationType().getMethod(methodName).invoke(annotation);

        } catch (InvocationTargetException e) {
            throw new IncompleteAnnotationException(annotation.annotationType(), methodName);
        } catch (NoSuchMethodException e) {
            throw new IncompleteAnnotationException(annotation.annotationType(), methodName);
        } catch (IllegalAccessException e) {
            throw new IncompleteAnnotationException(annotation.annotationType(), methodName);
        }

    }

    public static boolean isSingletonComponent(Class clas) {

        String scope = getComponentMethodValue(clas, SCOPE_STRING);
        return SINGLETON.equals(scope);
    }

    public static <T> T getCloneFrom(T instance) {

        Class instanceClass = instance.getClass();

        try {

            Method cloneMethod = instanceClass.getMethod(CLONE);
            cloneMethod.setAccessible(true);
            //noinspection unchecked
            return (T) cloneMethod.invoke(instance);

        } catch (Exception e) {
            throw new MContainerUncheckedException("The non Singleton class " + instanceClass + " is not cloneable.", e);
        }

    }


    public static boolean isComponent(Class cls) {
        return getComponent(cls) != null;
    }


    public static <T> T maybeSingleton(T resultingInstance) {

        if (resultingInstance == null)
            return null;

        if (!isSingletonComponent(resultingInstance.getClass()))
            return getCloneFrom(resultingInstance);

        return resultingInstance;
    }

    public static boolean isInstanciable(Class cls) {

        try {
            cls.newInstance();
            return true;
        } catch (InstantiationException e) {
            return false;
        } catch (IllegalAccessException e) {
            return false;
        }
    }

    public static String getPath(String propertyFileName, String location) {
        String path = propertyFileName;

        if (location != null)
            path = location + System.getProperty(FILE_SEPARATOR) + propertyFileName;
        return path;

    }

    /**
     * Obtains a pair of annotation class and value where the class is the same as the given one.
     *
     * @param annotations     vector of the method's parameter annotations
     * @param annotationClass desired annotation class
     * @return pair of annotation class and value or null if the annotation is not present
     */
    public static Pair<Class, String> getAnnotationClassAndValueForTag(Annotation annotations[], Class annotationClass) {
        Pair<Class, String> annotation = null;
        String annotationValue;

        try {
            for (Annotation annot : annotations) {

                if (annot.annotationType().equals(annotationClass)) {
                    annot.annotationType().getMethod(VALUE).setAccessible(true);
                    annotationValue = (String) annot.annotationType().getMethod(VALUE).invoke(annot);
                    annotation = new Pair<Class, String>(annotationClass, annotationValue);
                }

            }

            return annotation;

        } catch (InvocationTargetException e) {
            throw new MContainerUncheckedException("Annotation " + annotationClass.getName() + " class's method named \"value\" throws an exception", e.getTargetException());
        } catch (NoSuchMethodException e) {
            throw new MContainerUncheckedException("Annotation class " + annotationClass.getName() + " has no method named \"value\"", e);
        } catch (IllegalAccessException e) {
            throw new MContainerUncheckedException("Annotation " + annotationClass.getName() + " class's method named \"value\" is not accessible", e);
        }


    }


    /**
     * Get the pair of Interface class and Annotation (obtained from a setter method's parameter).
     *
     * @param method          setter method
     * @param annotationClass wanted annotation class
     * @return pair of Interface  class and Annotation
     */
    public static Pair<Class, Pair<Class, String>> getMappingBinded(Method method, Class annotationClass) {

        Pair<Class, String> annotation = getAnnotationClassAndValueForTag(method.getParameterAnnotations()[0], annotationClass);
        Class parameterType = method.getParameterTypes()[0];

        if (ContainerProvider.class.isAssignableFrom(parameterType)) {
            parameterType = (Class) ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
        }


        return new Pair<Class, Pair<Class, String>>(parameterType, annotation);


    }

    /**
     * Invokes a setter of a certain instance with a given parameter.
     *
     * @param instance  the instance whose setter is invoked
     * @param setter    the setter method
     * @param parameter the parameter given to the setter
     */
    public static void useSetter(Object instance, Method setter, Object parameter) {

        setter.setAccessible(true);
        try {
            setter.invoke(instance, parameter);
        } catch (IllegalAccessException e) {
            throw new MContainerUncheckedException("Don't have access to setter " + setter.getName(), e);
        } catch (InvocationTargetException e) {
            throw new MContainerUncheckedException("Setter " + setter.getName() + " throws an exception upon injection", e.getTargetException());
        }

    }

    public static <T> T makeProxy(T instance, Class proxyHandlerClass) {
        Object result;

        try {

            @SuppressWarnings("unchecked") Constructor<? extends ContainerProxyHandler> constructor = proxyHandlerClass.getDeclaredConstructor(Object.class);
            constructor.setAccessible(true);

            ContainerProxyHandler containerProxyHandler = constructor.newInstance(instance);
            result = containerProxyHandler.newProxyInstance();

        } catch (NoSuchMethodException e) {
            throw new MContainerUncheckedException("The proxy handler class does not have a corresponding constructor", e);
        } catch (InvocationTargetException e) {
            throw new MContainerUncheckedException("The proxy handler's constructor throws an Exception. ", e);
        } catch (InstantiationException e) {
            throw new MContainerUncheckedException("The proxy handler is not instantiable. ", e);
        } catch (IllegalAccessException e) {
            throw new MContainerUncheckedException("Can't access the proxy handler's constructor. ", e);
        }
        //noinspection unchecked
        return (T) result;

    }

    static Method[] getStaticMethods(Class cls) {
        Method methods[] = cls.getMethods();
        List<Method> staticMetods = new LinkedList<Method>();
        for (Method m : methods) {
            if (Modifier.isStatic(m.getModifiers()))
                staticMetods.add(m);
        }
        return staticMetods.toArray(new Method[staticMetods.size()]);


    }


    public static Method[] getWantedMethods(Object instance, ObjectType type) {
        Method methods[];
        switch (type) {
            case INSTANCE:
                methods = instance.getClass().getMethods();
                break;
            case CLASS:
                methods = getStaticMethods((Class) instance);
                break;
            default:
                methods = new Method[0];
        }

        return methods;

    }


    public static Properties loadProperties(String propertyFileName) throws MContainerUncheckedException {

        InputStream propertiesFile;

        try {

            if (propertyFileName.contains(System.getProperty(FILE_SEPARATOR)))
                propertiesFile = new FileInputStream(propertyFileName);
            else
                propertiesFile = Thread.currentThread().getContextClassLoader().getResourceAsStream(propertyFileName);
            Properties properties = new Properties();

            properties.load(propertiesFile);
            return properties;

        } catch (IOException e) {
            throw new MContainerUncheckedException("Can't load property file " + propertyFileName, e);
        }
    }



}
